﻿/*
 * ---------------------------------------------------------------------------
 * File: SmartIni.cs
 * Author: Dmitry Abdulov
 * Code License: GNU Lesser General Public License (LGPL)
 * Last changed: 26.04.2013
 * 
 * Do not remove and change this header.
 * ---------------------------------------------------------------------------
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;

namespace SmartIni
{
    public static class Ini
    {
        public const string DEFAULT_NODE_NAME = "__##DEFAULT##__";

        public static IniFile LoadData(StreamReader source)
        {
            var ini = new IniFile();
            LoadData(source, ini);
            return ini;
        }
        public static void LoadData(StreamReader source, IniFile ini)
        {
            IniNode currentNode = null;

            while (!source.EndOfStream)
            {
                var ln = source.ReadLine();
                var lineType = getLineType(ln);
                switch (lineType)
                {
                    case IniLineType.NormalValue:

                        // Do we have any node?
                        if (currentNode == null)
                            currentNode = ini.AddNode(DEFAULT_NODE_NAME);

                        // Add value to node with parsing:
                        var i = ln.IndexOf('=');
                        var key = ln.Substring(0, i);
                        var value = ln.Substring(++i, ln.Length - i);

                        // Cut comment from value:
                        i = value.IndexOf('#');
                        if (i != -1)
                            value = value.Substring(0, i);

                        // Insert into node's values list:
                        currentNode.Values.Add(key.Trim(), value == string.Empty ? null : value.Trim());

                        break;

                    case IniLineType.NodeBegin:

                        // Parse node name and cut comment:
                        var nodeName = ln.Substring(1, ln.Length - 2);
                        i = nodeName.IndexOf('#');
                        if (i != -1)
                            nodeName = nodeName.Substring(0, i);

                        // Switch to node:
                        currentNode = ini.AddNode(nodeName.Trim());

                        break;

                    case IniLineType.ListValue://todo: merge with case#0

                        // Do we have any node?
                        if (currentNode == null)
                            currentNode = ini.AddNode(DEFAULT_NODE_NAME);

                        // Cut comment from string:
                        i = ln.IndexOf('#');
                        if (i != -1)
                            ln = ln.Substring(0, i);

                        // Insert into node's values list:
                        currentNode.Values.Add(ln.Trim(), null);

                        break;

                    case IniLineType.EOF:
                        return; // End reading INI data:
                }
            }
        }
        public static void SaveData(IniFile ini, StreamWriter w, bool skipEmptyNodes = true)
        {
            var count = ini.Nodes.Count;
            for (var i = 0; i < count; i++)
            {
                var node = ini[i];
                if (skipEmptyNodes && node.Values.Count == 0)
                    continue;

                // Write node name:
                if (node.Name == DEFAULT_NODE_NAME && i == 0)
                    /* Do not write DEFAULT node name */
                    ;
                else
                    w.WriteLine('[' + node.Name + ']');

                // Write keys data:
                foreach (var e in node.Values)
                    /*if (e.Value == null)
                        w.WriteLine(e.Key);
                    else*/ foreach(var v in e.Value)
                               w.WriteLine(e.Key + (v != null ? " = " + v : null));

                // Newline:
                w.WriteLine();
            }
            w.Flush();
        }

        enum IniLineType
        {
            CommentOnly,
            NormalValue,
            ListValue,
            NodeBegin,
            EOF, Empty
        }
        static IniLineType getLineType(string line)
        {
            if (line.Length == 0)
                return IniLineType.Empty;
            else if (getFirstChar(line) == '#')
                return IniLineType.CommentOnly;
            else if (line.IndexOf('=') != -1 && line[0] != '=')
                return IniLineType.NormalValue;
            else if (line[0] == '[' && line.Length > 2 && line.IndexOf(']') != -1)
                return IniLineType.NodeBegin;
            else if (line.ToUpper().Trim() == "__##EOF_INI##__")
                return IniLineType.EOF;
            else
                return IniLineType.ListValue;
        }
        static char getFirstChar(string str)
        {
            for (int i = 0; i < str.Length; i++)
                if (str[i] != ' ')
                    return str[i];
                else continue;
            throw new Exception("WTF");
        }
    }

    public class IniFile : IEnumerable, IEnumerator
    {
        public readonly List<IniNode> Nodes = new List<IniNode>();

        public IniNode AddNode(string nodeName)
        {
            var node = new IniNode(nodeName);
            Nodes.Add(node);
            return node;
        }
        public IniNode this[string nodeName, int subId = 0]
        {
            get
            {
                var i = getNodeIndex(nodeName, subId);
                return i != -1 ? Nodes[i] : null;
            }
        }
        public IniNode this[int nodeIndex]
        {
            get { return Nodes[nodeIndex]; }
        }
        public IniNode DefaultNode
        {
            get
            {
                var node = this[Ini.DEFAULT_NODE_NAME];
                if (node == null)
                {
                    node = new IniNode(Ini.DEFAULT_NODE_NAME);
                    Nodes.Add(node);
                }
                return node;
            }
        }

        int getNodeIndex(string nodeName, int skip = 0)
        {
            var c = Nodes.Count;
            var skipped = 0;
            for (int i = 0; i < c; i++)
                if (Nodes[i].Name == nodeName)
                    if (++skipped > skip)
                        return i;
            return -1;
        }
        #region IEnumerable
        int enumPos = 0;
        public IEnumerator GetEnumerator()
        {
            return (IEnumerator)this;
        }
        public bool MoveNext()
        {
            enumPos++;
            if (enumPos < Nodes.Count)
                return true;
            else
                return false;
        }
        public void Reset()
        {
            enumPos = -1;
        }
        public object Current
        {
            get { return Nodes[enumPos]; }
        }
        #endregion
    }
    public class IniNode
    {
        public IniNode(string name) { this.Name = name; }
        public readonly ExtendedDictionary<string, string> Values = new ExtendedDictionary<string, string>();
        public string Name { get; set; }

        public string Get(string key, string defaultValue = null, int subId = -1)
        {
            if (!Values.ContainsKey(key))
                return defaultValue;
            else if (subId < 0)
                subId = Values.GetValues(key).Count + subId;

            return Values[key, subId];
        }
        public string this[string key, int subId = -1]
        {
            get { return this.Get(key, null, subId); }
            set
            {
                if (!Values.ContainsKey(key))
                {
                    Values.Add(key, value.ToString());
                    return;
                }
                else if (subId < 0)
                    subId = Values.GetValues(key).Count + subId;

                Values[key, subId] = value;
            }
        }
        public void Set(string key, string value, int subId = 0) { this[key, subId] = value; }

        // Get extensions:
        public Int32 GetInt32(string key, int defaultValue = -1, int subId = -1, bool raiseParseError = false)
        {
            Int32 val = defaultValue;
            string strVal = Get(key, defaultValue.ToString(), subId);
            if (!Int32.TryParse(strVal, out val) && raiseParseError)
                throw new FormatException("Failed to parse (Int32): " + strVal);
            return val;
        }
        public bool GetBool(string key, bool defaultValue = false, int subId = -1, bool raiseParseError = false)
        {
            var strVal = Get(key, defaultValue ? "1" : "0", subId);

            // Try number-style:
            short iVal = 0;
            if (short.TryParse(strVal, out iVal))
                return iVal != 0;

            // Try as boolean:
            bool bVal = defaultValue;
            if (!bool.TryParse(strVal, out bVal) && raiseParseError)
                throw new FormatException("Failed to parse (Boolean): " + strVal);
            return bVal;
        }
        public Version GetVersion(string key, Version defaultValue = null, int subId = -1, bool raiseParseError = false)
        {
            var strVal = Get(key, defaultValue != null ? defaultValue.ToString() : null, subId);

            try { return new Version(strVal); }
            catch (Exception ex)
            {
                if (raiseParseError)
                    throw new FormatException("Failed to parse (Version): " + strVal, ex);
            }
            return defaultValue;
        }

        // Set extensions:
        public void Set(string key, Int32 value, int subId = -1)
        {
            this[key, subId] = value.ToString();
        }
        public void Set(string key, bool value, int subId = -1) { this[key, subId] = value ? "1" : "0"; }
        public void Set(string key, Version value, int subId = -1) { this[key, subId] = value != null ? value.ToString() : null; }

        // Other:
        /// <param name="onlyNoValueKeys">Do not include keys which have value ?</param>
        public List<string> GetKeys(bool onlyNoValueKeys = false)
        {
            var list = new List<string>(Values.Count);
            foreach (var e in Values)
                if (!onlyNoValueKeys || e.Value != null)
                    list.Add(e.Key);
            return list;
        }
        public List<string> GetValuesList(string key)
        {
            if (!Values.ContainsKey(key))
                return new List<string>(0);
            return Values.GetValues(key);
        }
        public void RemoveValue(string key, int subId = 0)
        {
            Values[key].RemoveAt(subId);
            if (Values[key].Count == 0)
                Values.Remove(key);
        }
        #region IEnumerable
        int enumPos = 0;
        public IEnumerator GetEnumerator()
        {
            return (IEnumerator)this;
        }
        public bool MoveNext()
        {
            enumPos++;
            if (enumPos < Values.Count)
                return true;
            else
                return false;
        }
        public void Reset()
        {
            enumPos = -1;
        }
        public IniNode Current
        {
            get { return null; }
        }
        #endregion
    }

    public class ExtendedDictionary<TKey, TValue> : Dictionary<TKey, List<TValue>>
    {
        // This code is improved solution from here: 
        // http://stackoverflow.com/questions/1187219/is-there-a-dictionarystring-object-collection-which-allows-multiple-keys

        public void Add(TKey key, TValue value)
        {
            if (!ContainsKey(key))
                Add(key, new List<TValue>());
            this[key].Add(value);
        }
        public List<TValue> GetValues(TKey key)
        {
            return this[key];
        }
        public List<TValue> GetAllValues()
        {
            List<TValue> list = new List<TValue>();

            foreach (TKey key in this.Keys)
            {
                List<TValue> values = this.GetValues(key);
                list.AddRange(values);
            }

            return list;
        }

        public TValue this[TKey key, int subIndex]
        {
            get { return GetValues(key)[subIndex]; }
            set
            {
                var lst = this[key] as List<TValue>;
                lst[subIndex] = value;
            }
        }
    }
}
